using System;
using System.Runtime.Serialization;

namespace ICommonLibrary.Date {

    [DataContract]
    public sealed class YearMonth : IComparable {

        /// <summary>
        /// Returns a value between 1 and 9999.
        /// </summary>
        [DataMember]
        public int Year {
            get { return _year; }
            set { _year = value; Normalise(); }
        }

        /// <summary>
        /// Returns a value between 0 and 11.
        /// </summary>
        [DataMember]
        public int Month {
            get { return _month; }
            set { _month = value; Normalise(); }
        }

        private int _year;
	    private int _month;
	    private int _uniqueKey;
    	
	    public YearMonth() : this(DateTime.Now) {
	    }
    	
        /// <summary>
        /// 
        /// </summary>
        /// <param name="year">1-9999</param>
        /// <param name="month">0-11</param>
	    public YearMonth(int year, int month) {
		    _year = year;
		    _month = month;
    		
		    Normalise();
	    }
    	
	    public YearMonth(DateTime dateTime) : this(dateTime.Year, dateTime.Month - 1) {
	    }

        public static YearMonth FromUniqueKey(int uniqueKey) {
            return new YearMonth(uniqueKey / 10000, (uniqueKey % 10000) / 100);
        }

        private YearMonth(int year, int month, int uniqueKey) {
		    _year = year;
		    _month = month;
		    _uniqueKey = uniqueKey;
	    }

	    public YearMonth Clone() {
		    return new YearMonth(_year, _month, _uniqueKey);
	    }

        public int CompareTo(object o) {
            if (o == null) {
                return 1;
            }
            if (o == this) {
                return 0;
            }
            var d = o as YearMonth;
            if (d == null) {
                return 1;
            }

            return _uniqueKey - d._uniqueKey;
        }

        public bool Before(YearMonth m) {
		    return _uniqueKey < m._uniqueKey;
	    }
    	
	    public bool After(YearMonth m) {
		    return _uniqueKey > m._uniqueKey;
	    }
    	
	    public YearMonth AddMonths(int months) {
		    _month += months;
		    Normalise();
		    return this;
	    }
    	
	    public YearMonth AddYears(int years) {
		    _year += years;
		    Normalise();
		    return this;
	    }
    	
	    public int GetUniqueKey() {
		    return _uniqueKey;
	    }
 
	    public int HashCode() {
		    return _uniqueKey;
	    }
    	
	    public bool Equals(YearMonth m) {
		    return _uniqueKey == m._uniqueKey;
	    }

        public override int GetHashCode() {
            return _uniqueKey;
        }

        public override bool Equals(Object o) {
		    if (o == this) return true;
		    if (o != null && o.GetType() == typeof(YearMonth)) {
			    var m = (YearMonth) o;
			    return _uniqueKey == m._uniqueKey;
		    }
		    return false;
	    }

	    public MutableDate GetFirstDay() {
            return new MutableDate(_year, _month, 1);
	    }

        public MutableDate GetLastDay() {
            return new MutableDate(_year, _month, DateUtil.GetDaysOfMonth(this));
        }

        public override string ToString() {
            return _year + "-" + (_month + 1);
        }

        /**
	     * This method may contains some boilerplate code that is to help it run as
	     * fast as it can.
	     */
	    private void Normalise() {

		    // normalise month
		    if (_month > 11 || _month < 0) {
			    NormaliseMonth();
		    }
		    _uniqueKey = GenerateUniqueKey(_year, _month);
	    }
    	
	    private static int GenerateUniqueKey(int y, int m) {
		    return y * 10000 + m * 100;
	    }

	    private int NormaliseMonth() {
		    int m = _month;
		    if (m > 11) {
			    int delta = m % 12;
			    _year += m / 12;
			    m = _month = delta;
		    }
		    else if (m < 0) {
			    int delta = m % 12;
			    _year += m / 12;
			    if (delta != 0) {
				    _year--;
				    m = _month = 12 + delta;
			    }
			    else {
				    m = _month = 0;
			    }
		    }
		    return m;
	    }
            
    }
}